In PHP Magazine 5.2013, I discussed the evolution of PHP security over time [1]. There were two outstanding developments then: in PHP 5.5, an API for calculating password hashes was introduced, while the classic MySQL extension [2] was branded as deprecated, or obsolete. Meanwhile, it has become history: it was removed in PHP 7.0. Which is why I would like to start off with just that.
MySQL is dead, long live MySQLi!
If you used the MySQL extension, you’ve had to switch to MySQLi [3] or PDO_MySQL [4] (or had to look for another database, but hardly anyone would have done that). This became quite easy in the case of MySQLi and I hope you have not taken this easy path. Or at least had gone one step further.
From MySQL to MySQLi – the easy way
If you want to convert a database query with the MySQL extension, such as the one in Listing 1, into a query using MySQLi, not much changes:
- When setting up the connection to the database server, in MySQLi you also enter the desired database which was selected separately in the MySQL extension.
- All mysql_ calls become corresponding mysqli_ calls.
The result (Listing 2) looks very similar, the changes are within manageable limits and are done relatively quickly – running a search/replace is often all you need to do. The code in Listing 2 is just as secure or insecure as the one in Listing 1, because its security depends solely and exclusively on whether the value of the $ort parameter contains an SQL injection attack. So as long as $ort cannot be manipulated by a possible attacker, or the input has been correctly filtered or masked, there is no danger. If that is not the case, both listings provide a lovely SQL injection vulnerability.
IPC NEWSLETTER
All news about PHP and web development
// establish connection to MySQL server $connection = mysql_connect($dbserver,$dbuser,$dbpassword); if ($connection) { // connection to MySQL server exists // select database $db = mysql_select_db($dbname); if ($db) { // Connection to the database exists // Query data $query = "SELECT username FROM user_table WHERE location = '".$ort."'"; $result = mysql_query($query); if ($result) { // query successful while( $row = mysql_fetch_array($result) ) { // output data or otherwise process // ... } } else { // error fetching the data echo "Error fetching data: \n"; echo "MySQL error “.mysql_errno().": “.mysql_error()." \n"; } } else { // No connection to database echo "Error connecting to the database: \n"; echo "MySQL error: “.mysql_errno().": “.mysql_error()." \n"; } } else { // No connection to MySQL server echo "Error connecting to MySQL server: \n"; echo "MySQL error: “.mysql_errno().": “.mysql_error()." \n"; }
// establish connection to the database $db = mysqli_connect($dbserver, $dbuser, $dbpassword, $dbname); if ($db) { // Connection to the database exists // Query data $query = "SELECT username FROM user_table WHERE location = '".$ort."'"; $result = mysqli_query($db, $query); if ($result) { // query successful while( $row = mysqli_fetch_array($result) ) { // output data or otherwise process // ... } } else { // error fetching the data echo "Error fetching data: \n"; echo "MySQL error: ".mysqli_errno().": ".mysqli_error()." \n"; } } else { // no connection to MySQL echo "Error connecting to MySQL server: \n"; echo "MySQL error: “.mysqli_connect_errno().": “.mysqli_connect_error()." \n"; }
Prepared statements with parameter binding prevent SQL injection
Parameterized calls of prepared statements or stored procedures offer reliable protection against SQL injection – these are also referred to as parameter binding or parameterized queries. Here the structure of the SQL statement is split into two steps:
- The structure of the SQL statement is defined, while placeholders are used for all entries.
- The content of the placeholders is specified.
The data inserted in the second step has no possibility of changing the structure of the statement defined in the first step. After the structure of the statement has been defined, any data passed to the placeholders is considered as data rather than a part of the statement structure. If an attacker should inject SQL code, it will not be executed. While in Listing 1 and Listing 2, an input such as
Anywhere 'OR 1 = 1
would result in the output of all user names, nothing would be output during a parameterized call of a prepared statement. This is unless there would be an entry in the location column which would be identical to the entry – then the corresponding line would be output. This is however extremely unlikely.
Prepared statements are only safe if used consistently for all SQL queries. If they are only used for queries that contain direct user input, a second order SQL injection is possible – here the SQL injection code is first inserted into the database and only executed when the prepared entry is used later.
From MySQL to MySQLi – the secure way
Using prepared statements with parameterized calls is not a big issue with MySQLi, so the query in Listing 1 could look like the one in Listing 3.
// establish connection to the database $db = mysqli_connect($dbserver, $dbuser, $dbpassword, $dbname); if ($db) { // Connection to the database exists // Step 1: prepare the query as a prepared statement $statement = mysqli_prepare($db, "SELECT username FROM user_table WHERE location = ?"); // Step 2: bind the parameters to it mysqli_bind_param($statement, 's', $ort); // Step 3: fill the parameters $ort = $_POST["location"]; // Query data $ok = mysqli_stmt_execute($statement); if ($ok) { // Query successful // Bind variables to the result mysqli_stmt_bind_result($statement, $username); while( mysqli_stmt_fetch($statement) ) { // output data or otherwise process // ... } } else { // error fetching the data echo "Error fetching data: \n"; echo "MySQL error: ".mysqli_errno().": ".mysqli_error()." \n"; } } else { // no connection to MySQL echo "Error connecting to MySQL server: \n"; echo "MySQL error: “.mysqli_connect_errno().": “.mysqli_connect_error()." \n"; }
The second parameter of the mysqli_bind_param () function indicates what data types the parameters of the SQL statement have – in this case both are strings. The four format specifications listed in Table 1 are available.
Formatstring | data type |
---|---|
I | integer values |
D | Double and float values |
B | BLOBs |
S | Strings |
Table 1: Format specifications for parameterized prepared statements
For the sake of simplicity, I have left out further processing of the query result in all three listings – you can manage that yourself.
Don’t make it too easy, please!
Even if the prepared statement is protected against SQL injection through the parameterized call, you should still check the parameters for possible malicious code and filter or mask the input, and in no case should you apply them directly, as I had done in Listing 3. Depending on the type of query, unpleasant side effects could arise – for example, if the SQL injection code would be planted in an INSERT call. Although the attacker would not be able to manipulate the INSERT call itself, the injected code would be written to the database. This in turn could lead to the second order SQL injection mentioned above during later use if the trusted data from your own database is used for an unprotected call without rechecking/filtering.
You should also keep in mind that the data entered by your application into the database may potentially be further processed by a third-party web application that does not verify this data because it comes from what it considers to be a trusted source. Therefore, always check/filter and store only unambiguously harmless data in the database – then all third-party users are also protected from attacks that would otherwise be ascribed to your application.
IPC NEWSLETTER
All news about PHP and web development
MySQL is dead, long live PDO_MySQL!
Instead of MySQLi you can also use PDO_MySQL as a replacement for the old MySQL extension. MySQLi and PDO_MySQL are not that different in functionality; the biggest difference is that PDO can only be used in an object-oriented manner, which is why it is not possible to simply transfer the above examples, but the code in Listing 4 serves the same purpose. Using a prepared statement – for safety reasons, of course.
try { // establish connection to the database $db = new PDO('mysql:host='.$dbserver.';dbname='.$dbname.;charset=utf8', '$dbuser', '$dbpassword'); // Step 1: prepare the query as a prepared statement $statement = $db->("SELECT username FROM user_table WHERE location = ?") // Step 2: bind the parameters to it $statement->bindParam(1, $ort); // Step 3: fill the parameters $ort = $_POST["location"]; // Query data as an associative array $statement->setFetchMode(PDO::FETCH_ASSOC); while($row = $statement->fetch()) { // output data or otherwise process // ... } } catch(PDOException $exception) { echo $exception->getMessage(); }
In addition to the fact that PHP 7.x. will continue to be supported with vulnerability patches, there are also a few security-related enhancements.
New password hashing algorithm
As I had already mentioned in [1], PHP 5.5 had introduced an API for calculating password hashes. In PHP 7.2, this was extended with a new algorithm [5]: Argon2 It is a powerful algorithm for hashing passwords and was also the winner of the Password Hashing Competition [6]. Similar to the research for the DES successor AES and the SHA2 successor SHA3, a search was made between 2013 and 2015 for a particularly secure password hashing algorithm.
This provides an alternative to the bcrypt, the only algorithm available to date. For a description of the password hashing API [7], see [1]; to find out why these special functions are needed, see [8].
I’d like to note one thing at this point: when calling the password hashing functions, you can choose between the default algorithm PASSWORD_DEFAULT or a specific algorithm (PASSWORD_BCRYPT, PASSWORD_ARGON2I and PASSWORD_ARGON2ID). At first glance, the default algorithm seems to be a good choice, but upon closer inspection, not so much: up till now, bcrypt has been the default algorithm. At some point, the more secure Argon2 algorithm will certainly become the default algorithm. And then, all the applications that use PASSWORD_DEFAULT will have a problem: the password check fails because of course the values hashed using bcrypt cannot be checked by argon2.
To prevent this from happening to your applications, you should always set the desired algorithm yourself.
Crypto Libraries: one out, one in
Also in PHP 7.2, the Sodium (Libsodium) crypto library[9] has become a standard library [10]. Libsodium has been a component of PECL for some time now and provides developers with the usual cryptographic functions, such as hash functions, encryption and decryption, and signatures. And all that in the current versions. So you have, for example, the Diffie-Hellman method for key exchange and the DSA algorithm on elliptic curves (Curve25519), the ChaCha20 encryption method and the Poly1305 Message Authentication Code (MAC). Hash functions include Argon2 or Blake2, for example.
If you want to know more on which cryptographic methods you should or should not use, you can read up on that in my article in PHP Magazine 2.2018 and on entwickler.de [11]. Some insecure procedures or their implementations have already been removed from PHP by declaring the obsolete and no longer maintained Mcrypt extension as deprecated in PHP 7.1 and moving it out of the core in PECL in PHP 7.2 [12].
No coincidence at all: new random numbers
Since PHP 7.0.0, a new pseudorandom number generator for generating random values which can be used for cryptographic purposes has been available: CSPRNG [13]. It contains only two functions (and more are not needed either): With random_bytes () you can generate random byte sequences, while random_int () creates random integer values.
Improvements in serialization
In PHP 7.0.0 and 7.1.0, the security of unserialize() [14] has been improved.
The options parameter introduced in PHP 7.0.0 allows you to add options. The only one that existed up till then is called allowed_classes and is used to define allowed class names. The value can be either an array with the class names that should be accepted, FALSE to accept none of them, and TRUE to accept all the classes.
Since PHP 7.1.0, the allowed_classes element of options is strictly standardized: If something other than an array or Boolean value is passed, unserialize () returns a FALSE value and triggers an E_WARNING.
Of course, this does not change the fact that an insecure serialization is ranked 8th in the current OWASP Top 10 [15], but it does allow for better hedging of deserialization.
A few odds and ends
The defaults for SSL/TLS have been improved in PHP 7.1.0:ssl:// is now an alias for tls:// and its default are now TLSv1.0 , TLSv1.1 or TLSv1.2. And the default for STREAM_CRYPTO_METHOD_TLS_* is now TLSv1.0 or TLSv1.1 + TLSv1.2 instead of just TLSv1.0.
The (crypto) functions hash_hmac(), hash_hmac_file(), hash_pbkdf2() and hash_init() (with HASH_HMAC) have been accepting only cryptographic hash functions since PHP 7.1.0, instead of general as before.
In addition, some features or options have been declared as deprecated for security reasons, such as the create_function() function in PHP 7.2.0 (which is a wrapper for the known evil eval()) [16] and the call of the parse_str () function without a second argument (because that leads to the same security issues as the use of register_globals) [17]. Also the call of assert() using a string as a parameter is considered deprecated [18] because the eval() string must be transferred, which leads to the risk of remote code execution.
Conclusion
The biggest issue for PHP 5.6 (and all older versions) is the lack of support. Quite a few vulnerabilities, including critical ones, have been found and fixed in PHP. Does anyone really believe that there won’t be any more found in the future? That’s what it looks like, because as I was doing research for this article, I came across a hoster who actually wants to offer PHP 5.2 (!) to PHP 5.6 to customers until January 1, 2021(!). Offering a version for another two years that has not been supported for eight years (support end for PHP 5.2 was on 1/06/2011) – now that’s what I call an athletic spirit. And that is not the only hoster offering versions that have long since not been supported.
What do these hosters intend to do if a critical vulnerability is found in such a version and first attacks on it appear? Because one thing is certain: such vulnerabilities are no longer being resolved [19]. Version 7.0 isn’t supported in this regard either, incidentally. Perhaps some Linux distributions will backport PHP 5.6 and 7.0 patches for their long-time support versions for some time – if they didn’t pay attention and didn’t remove the version in time. But there will be no more patches from the PHP developers themselves.
Of course, the hosters are stuck in the mud here: you can’t just shut down PHP 5.6 while your customers are still using it. And they usually take their time in switching to a new version. So the hosters have to at least partially give them that time. Putting pressure on customers is not an option either, as they may prefer to switch to a host that continues to support their web applications rather than a PHP version that requires changes to the web application that they may not be able to make. But they’re still playing with fire.
I think it’s safer to go with Murphy: “Anything that can go wrong will go wrong”, and add “sooner or later” to that. And if Murphy’s Law also applies to Murphy’s Law, then it’s “rather sooner than later”! The problem here though is that no one knows if and when a vulnerability has been found and what kind of vulnerability it is. But since my main concern is security, I just have to assume the worst case scenario and have a solution ready. Here the worst case would be a vulnerability that would allow for remote code execution. Without prior authentication and without any special conditions that would have to be fulfilled. In a component that is needed and that cannot be easily removed.
It is really unlikely that this should happen, but what could you do when it comes to that? The only possible solution: pull all servers running old PHP versions from the Internet. Your customers will most likely not be overly thrilled. It’s quite clearly their own fault though, as they could have switched to a current version. But they will not want to hear that. I hope that we will be spared this fate.
At the end of support for Windows XP, everyone expected that cybercriminals would be sitting on mountains of zero-day exploits, just waiting for the support-end green light to let it all go. But in the end, the huge wave of bugs and attacks failed to appear. It wasn’t until a few years later that the malicious program WannaCry appeared – and Microsoft was forced to publish an update long after support end.
Links & literature
[1] Eilers, Carsten: “PHP-Sicherheit von PHP 4.x bis PHP 5.5”; PHP Magazin 5.2013
[2] PHP Manual: MySQL (Original): http://php.net/manual/en/intro.mysql.php
[3] PHP Manual: MySQLi: http://php.net/manual/en/book.mysqli.php
[4] PHP Manual: PDO_MYSQL: http://php.net/manual/en/ref.pdo-mysql.php
[5] PHP RFC: Argon2 Password Hash: https://wiki.php.net/rfc/argon2_password_hash
[6] Password Hashing Competition and our recommendation for hashing passwords: Argon2: https://password-hashing.net
[7] PHP Manual: Password Hashing: http://php.net/manual/en/book.password.php
[8] Eilers, Carsten: “Passwörter speichern, aber richtig!”; PHP Magazin 6.2013
[9] Libsodium Documentation: https://libsodium.gitbook.io/doc/
[10] PHP RFC: Make Libsodium a Core Extension: https://wiki.php.net/rfc/libsodium
[11] Eilers, Carsten: “Ein schmaler Grat”; PHP Magazin 1.2018, also online on entwickler.de: https://entwickler.de/online/security/kryptoverfahren-579848799.html
[12] PHP RFC: Deprecate (then Remove) Mcrypt: https://wiki.php.net/rfc/mcrypt-viking-funeral
[13] PHP Manual: CSPRNG: http://php.net/manual/de/book.csprng.php
[14] PHP Manual: unserialize: http://php.net/manual/de/function.unserialize.php
[15] Eilers, Carsten: “Zehn Bedrohungen für Webanwendungen Teil 4: Cross-Site Scripting, Insecure Deserialization”; PHP Magazin 6.2018
[16] PHP Manual: create_function: http://php.net/manual/de/function.create-function.php
[17] PHP Manual: parse_str: http://php.net/manual/en/function.parse-str.php
[18] PHP Manual: assert: http://php.net/manual/de/function.assert.php
[19] PHP: Supported Versions: http://php.net/supported-versions.php